using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Graphics;
using TiledLib;

namespace ContentPipelineExtension
{
    /// <summary>
    /// Processes a TMX file (Tiled Map Editor) into an appropriate run time type.
    /// </summary>
    [ContentProcessor(DisplayName = "ContentPipelineExtensions.TMX Processor")]
    public class TiledMapProcessor : ContentProcessor<MapContent, TempMapContent>
    {
        public override TempMapContent Process(MapContent input, ContentProcessorContext context)
        {
            TiledHelpers.BuildTileSetTextures(input, context, "Sprites/Tiles");
            TiledHelpers.GenerateTileSourceRectangles(input, "Sprites/Tiles");

            TempMapContent mapOutput = new TempMapContent
            {
                TileWidth = input.TileWidth,
                TileHeight = input.TileHeight,
                GridPosition = Vector2.Zero
            };

            foreach (LayerContent layer in input.Layers)
            {
                // if this is a tile layer, we need to create the layer based on texture
                if (layer is TileLayerContent)
                {
                    TileLayerContent tileLayerContent = layer as TileLayerContent;

                    TempLayerContent outLayer = CreateLayerContent(input, tileLayerContent);

                    mapOutput.Layers.Add(outLayer);
                }
                // if this is an object layer, we need to either get path data or generic object data
                else if (layer is MapObjectLayerContent)
                {
                    MapObjectLayerContent mapObjectLayer = layer as MapObjectLayerContent;

                    // if this is a path layer, pull the path data out and add the paths to the map object
                    if (mapObjectLayer.Name == "Paths")
                    {
                        List<PathContent> paths = GetPaths(mapObjectLayer);
                        mapOutput.Paths.AddRange(paths);
                    }
                    // otherwise create the object layer and add it to our generic object layer collection
                    else
                    {
                        TempMapObjectLayerContent outLayer = CreateObjectLayerContent(mapObjectLayer);
                        mapOutput.MapObjectLayers.Add(outLayer);
                    }
                }
            }

            return mapOutput;
        }

        private TempLayerContent CreateLayerContent(MapContent input, TileLayerContent tileLayerContent)
        {
            TempLayerContent outLayer = new TempLayerContent
            {
                Width = tileLayerContent.Width,
                Height = tileLayerContent.Height
            };

            // loop through all the tiles in this layer to build all the appropriate data
            outLayer.Tiles = new List<TempTileContent>(tileLayerContent.Data.Length);
            for (int i = 0; i < tileLayerContent.Data.Length; i++)
            {
                uint tileID = tileLayerContent.Data[i];

                // based on the tileID from the layer data, get its index and sprite effects
                int tileIndex;
                SpriteEffects spriteEffects;
                TiledHelpers.DecodeTileID(tileID, out tileIndex, out spriteEffects);

                ExternalReference<Texture2DContent> textureContent = null;
                Rectangle sourceRectangle = new Rectangle();

                // loop through the tilesets in order to extract texture data, source rectangle, and collidable property values
                foreach (TileSetContent tileSet in input.TileSets)
                {
                    if (tileIndex - tileSet.FirstId < tileSet.Tiles.Count && tileIndex > 0)
                    {
                        textureContent = tileSet.Texture;
                        sourceRectangle = tileSet.Tiles[(int)(tileIndex - tileSet.FirstId)].Source;

                        break;
                    }
                }

                // save the texture, source rectangle of the texture, the sprite effects, and the collidable property to our layer
                TempTileContent tempTile = new TempTileContent
                {
                    Texture = textureContent,
                    SourceRectangle = sourceRectangle,
                    SpriteEffects = spriteEffects
                };

                outLayer.Tiles.Add(tempTile);
            }

            return outLayer;
        }

        /// <summary>Paths are defined in a separate object layer in the .tmx file. Each path layer has separate objects that make up the destinations.
        /// A path is made by joining together each object with a similar name.
        /// The path should be named in the format of <Path#>-<Destination#>.
        /// For example a path can be named "1-2" where "1" is the path number and "2" means it's the second destination in the path.
        /// Every object with the same "Path#" will be joined to create a single path object.
        /// When the map collection is loaded, this path will be joined up with whatever entities are assigned to follow it.
        /// </summary>
        /// <param name="mapOutput"></param>
        /// <param name="mapObjectLayer"></param>
        private List<PathContent> GetPaths(MapObjectLayerContent mapObjectLayer)
        {
            // this dictionary contains a path (by id) and a collection of vectors that make up that path (also by id)
            // for example, path 1 can contain a collection of destinations 1, 2, 3, 4, 5
            Dictionary<int, Dictionary<int, Vector2>> pathDictionary = new Dictionary<int, Dictionary<int, Vector2>>();

            foreach (MapObjectContent mapObjectContent in mapObjectLayer.Objects)
            {
                // destination point is the center of the bounds
                Vector2 destination = new Vector2(mapObjectContent.Bounds.Center.X, mapObjectContent.Bounds.Center.Y);

                // object is structured like 'X-Y' where 'X' is the path ID and 'Y' is the destination ID
                // 1-1 is path 1, destination 1
                // 1-2 is path 1, destination 2
                String[] pathData = mapObjectContent.Name.Split('-');
                int pathId = Int32.Parse(pathData[0]);
                int destinationId = Int32.Parse(pathData[1]);

                // get the collection of destinations that make up this path
                Dictionary<int, Vector2> destinationDictionary = new Dictionary<int,Vector2>();
                bool doesPathExist = pathDictionary.TryGetValue(pathId, out destinationDictionary);

                // if we found a collection of destinations, add the current destination to the collection for this path
                if (doesPathExist)
                    destinationDictionary.Add(destinationId, destination);
                // if we did not find a collection of destinations, create one, add its id/value to the collection, and then add the collection to the path
                else
                {
                    Dictionary<int, Vector2> newDestinationDictionary = new Dictionary<int, Vector2>();
                    newDestinationDictionary.Add(destinationId, destination);
                    pathDictionary.Add(pathId, newDestinationDictionary);
                }
            }

            List<PathContent> pathOutput = new List<PathContent>();

            // iterate over the path to sort the destinations by their id
            foreach(int pathId in pathDictionary.Keys)
            {
                Dictionary<int, Vector2> newDestinationDictionary = pathDictionary[pathId];

                // this will sort the destinations by their id (this is needed in case we load in the destinations out of order)
                List<int> destinationIds = newDestinationDictionary.Keys.ToList();
                destinationIds.Sort();

                // after sorting the destinations, create the path based on its id and the sorted vector values
                PathContent path = new PathContent();
                path.Id = pathId;
                foreach (int id in destinationIds)
                    path.Destinations.Add(newDestinationDictionary[id]);

                pathOutput.Add(path);
            }

            return pathOutput;
        }

        /// <summary>Object layers make up the logical portion of the map. One object layer defines objects with collisions. 
        /// If the object layer is named "Collisions", then all objects under that layer are assumed to be collidable.
        /// Objects can also have a subscriber id that is used to subscribe to certain triggers. When the trigger fires,
        /// any subscribers that are joined up with the trigger are notified and they take appropriate action.
        /// </summary>
        /// <param name="mapObjectLayer"></param>
        /// <returns></returns>
        private TempMapObjectLayerContent CreateObjectLayerContent(MapObjectLayerContent mapObjectLayer)
        {
            TempMapObjectLayerContent outLayer = new TempMapObjectLayerContent
            {
                Name = mapObjectLayer.Name
            };

            foreach (MapObjectContent mapObjectContent in mapObjectLayer.Objects)
            {
                TempMapObjectContent outObject = new TempMapObjectContent()
                {
                    Bounds = mapObjectContent.Bounds,
                    Name = mapObjectContent.Name,
                    Type = mapObjectContent.Type
                };

                // this is a collidable layer, so set the object as collidable
                if (mapObjectLayer.Name == "Collisions")
                    outObject.IsCollidable = true;

                PropertyCollection objectProperties = mapObjectContent.Properties;

                // get a subscriber id for this object (if it returns 0, there is no subscriber id for this object)
                outObject.Id = GetSubscriberId(objectProperties);

                outLayer.MapObjects.Add(outObject);
            }

            return outLayer;
        }

        /// <summary>Objects can also have a subscriber id that is used to subscribe to certain triggers. When the trigger fires,
        /// any subscribers that are joined up with the trigger are notified and they take appropriate action.
        /// </summary>
        /// <param name="objectProperties"></param>
        /// <returns>(if it returns 0, there is no subscriber id for this object)</returns>
        private int GetSubscriberId(PropertyCollection objectProperties)
        {
            int tryParseResultInt;
            String propertyName = "SubscriberId";
            if (objectProperties.Keys.Contains(propertyName))
            {
                bool idValue = int.TryParse(objectProperties[propertyName], out tryParseResultInt);
                if (idValue)
                {
                    if (tryParseResultInt <= 0 || tryParseResultInt >= Int32.MaxValue)
                        throw new ArgumentOutOfRangeException(propertyName + " property was found on a tile, but the value was less than or equal to 0.");

                    return tryParseResultInt;
                }
                else
                    throw new NotFiniteNumberException(propertyName + " property was found on a tile, but the value not a valid number.");
            }
            else
                return 0;
        }
    }
}